/*
 * Copyright (C) 2012-2013 Michael L.R. Marques
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 * 
 * Contact: michaellrmarques@gmail.com
 */

package com.jm.commons.fio;

import com.jm.commons.arrays.ArrayUtils;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.imageio.ImageIO;

/**
 * File System operations
 * 
 * @author Michael L.R. Marques
 */
public class FileSystem {

    /**
     * File path constant
     */
    public static final int FILE_PATH = 0;
    
    /**
     * File name constant
     */
    public static final int FILE_NAME = 1;
    
    /**
     * File extension constant
     */
    public static final int FILE_EXT = 2;
    
    /**
     * 
     * @param list
     * @param dir
     * @param fileTypes
     * @return
     * @throws IOException 
     */
    public static List<File> listFiles(List<File> list, File dir, final String[] fileTypes) {
        return listFiles(list, dir, fileTypes, false, false);
    }
    /**
     * Provides the ability to list files in multiple files or folders,
     * {@code List<File>} list - the list of files to be returned
     * {@code File[]} files - an array of files or folders to check
     * {@code String[]} fileTypes - an array of extensions, this limits the
     * files types to be returned
     * {@code boolean} recursive - checks inside folders that may be found while
     * listing the files and folders
     *
     * @param list
     * @param files
     * @param fileTypes
     * @param recursive
     * @return List<File>
     */
    public static List<File> listFiles(List<File> list, File[] files, final String[] fileTypes, boolean recursive, boolean includeDirectorys) {
        return listFiles(list, Arrays.asList(files), fileTypes, recursive, includeDirectorys, null, false, false);
    }

    /**
     * Provides the ability to list files in multiple files or folders
     * {@code List<File>} list - the list of files to be returned
     * {@code List<File>} files - a list of files or folders to check
     * {@code String[]} fileTypes - an array of extensions, this limits the
     * files types to be returned
     * {@code boolean} recursive - checks inside folders that may be found while
     * listing the files and folders
     *
     * @param list
     * @param files
     * @param fileTypes
     * @param recursive
     * @return List<File>
     */
    public static List<File> listFiles(List<File> list, List<File> files, final String[] fileTypes, boolean recursive, boolean includeDirectorys) {
        return listFiles(list, files, fileTypes, recursive, includeDirectorys, null, false, false);
    }

    /**
     * Provides the ability to list files in multiple files or folders, while
     * having the ability to list files that contain a string
     * {@code List<File>} list - the list of files to be returned
     * {@code File[]} files - an array of files or folders to check
     * {@code String[]} fileTypes - an array of extensions, this limits the
     * files types to be returned
     * {@code boolean} recursive - checks inside folders that may be found while
     * listing the files and folders
     *
     * @param list
     * @param files
     * @param fileTypes
     * @param recursive
     * @param search
     * @param caseSensitive
     * @return List<File>
     */
    public static List<File> listFiles(List<File> list, File[] files, final String[] fileTypes, boolean recursive, boolean includeDirectorys, String search, boolean caseSensitive, boolean regex) {
        return listFiles(list, Arrays.asList(files), fileTypes, recursive, includeDirectorys, search, caseSensitive, regex);
    }

    /**
     * Provides the ability to list files in multiple files or folders, while
     * having the ability to list files that contain a string
     * {@code List<File>} list - the list of files to be returned
     * {@code List<File>} files - a list of files or folders to check
     * {@code String[]} fileTypes - an array of extensions, this limits the
     * files types to be returned
     * {@code boolean} recursive - checks inside folders that may be found while
     * listing the files and folders
     *
     * @param list
     * @param files
     * @param fileTypes
     * @param recursive
     * @param search
     * @param caseSensitive
     * @return List<File>
     */
    public static List<File> listFiles(List<File> list, List<File> files, final String[] fileTypes, boolean recursive, boolean includeDirectorys, String search, boolean caseSensitive, boolean regex) {
        list = list != null ? list : new ArrayList<File>();
        
        for (File file : files) {
            if (file.isDirectory()) {
                list = listFiles(list, file, fileTypes, recursive, includeDirectorys, search, caseSensitive, regex);
            } else if (file.isFile()) {
                if (isValidExtension(file, fileTypes)) {
                    if ((search == null || search.isEmpty()) ||
                            (regex && removeExtension(file.getName()).matches(search))  ||
                                (caseSensitive ? file.getName().contains(search) : file.getName().toUpperCase().contains(search.toUpperCase()))) {
                        list.add(file);
                    }
                }
            }
        }
        return list;
    }

    /**
     *
     * @param list
     * @param dir
     * @param fileTypes
     * @param recursive
     * @param includeDirectorys
     * @return
     */
    public static List<File> listFiles(List<File> list, File dir, final String[] fileTypes, boolean recursive, boolean includeDirectorys) {
        return listFiles(list, dir, fileTypes, recursive, includeDirectorys, null, false, false);
    }

    /**
     * Provides the ability to list files in multiple files or folders, while
     * having the ability to list files that contain a string
     * {@code List<File>} list - the list of files to be returned
     * {@code File} dir - the directory that will get checked
     * {@code String[]} fileTypes - an array of extensions, this limits the
     * files types to be returned
     * {@code boolean} recursive - checks inside folders that may be found while
     * listing the files and folders
     *
     * @param list
     * @param dir
     * @param fileTypes
     * @param recursive
     * @param search
     * @param caseSensitive
     * @return List<File>
     */
    public static List<File> listFiles(List<File> list, File dir, final String[] extensions, boolean recursive, boolean includeDirectorys, String search, boolean caseSensitive, boolean regex) {
        return listFiles(list, dir, new ExtensionFileFilter(extensions, includeDirectorys ? true : recursive), recursive, includeDirectorys, search, caseSensitive, regex);
    }
    
    /**
     * Provides the ability to list files in multiple files or folders, while
     * having the ability to list files that contain a string
     * {@code List<File>} list - the list of files to be returned
     * {@code File} dir - the directory that will get checked
     * {@code String[]} fileTypes - an array of extensions, this limits the
     * files types to be returned
     * {@code boolean} recursive - checks inside folders that may be found while
     * listing the files and folders
     * 
     * @param list
     * @param dir
     * @param genericFileFilter
     * @param recursive
     * @param includeDirectorys
     * @return 
     */
    public static List<File> listFiles(List<File> list, File dir, FileFilter fileFilter, boolean recursive, boolean includeDirectorys) {
        return listFiles(list, dir, fileFilter, recursive, includeDirectorys, null, false, false);
    }
    
    /**
     * Provides the ability to list files in multiple files or folders, while
     * having the ability to list files that contain a string
     * {@code List<File>} list - the list of files to be returned
     * {@code File} dir - the directory that will get checked
     * {@code String[]} fileTypes - an array of extensions, this limits the
     * files types to be returned
     * {@code boolean} recursive - checks inside folders that may be found while
     * listing the files and folders
     *
     * @param list
     * @param dir
     * @param fileTypes
     * @param recursive
     * @param search
     * @param caseSensitive
     * @return List<File>
     */
    public static List<File> listFiles(List<File> list, File dir, FileFilter fileFilter, boolean recursive, boolean includeDirectorys, String search, boolean caseSensitive, boolean regex) {
        list = list != null ? list : new ArrayList<File>();
        
        File[] files = dir.listFiles(fileFilter);
        if (files == null) {
            return list;
        }
        for (File file : files) {
            if (file.isDirectory()) {
                if (includeDirectorys) {
                    list.add(file);
                }
                if (recursive) {
                    list = listFiles(list, file, fileFilter, recursive, includeDirectorys, search, caseSensitive, regex);
                }
            } else if (file.isFile()) {
                if ((search == null || search.isEmpty()) ||
                        (regex && removeExtension(file.getName()).matches(search))  ||
                            (caseSensitive ? file.getName().contains(search) : file.getName().toUpperCase().contains(search.toUpperCase()))) {
                    list.add(file);
                }
            }
        }
        return list;
    }
    
    /**
     * Sorts a list of files alphabetically
     * {@code List<File>} files - list of files to be sorted
     * {@code int} low - the lowest element to sort from
     * {@code int} n - the highest element to sort to
     * {@code int} by - sets how to sort the files, by: - FILE_PATH = 0 -
     * FILE_NAME = 1 - FILE_EXT = 3
     *
     * @param files
     * @param low
     * @param n
     * @param by
     * @return List<File>
     */
    public static List<File> sort(List<File> files, int low, int n, int by) {
        int lo = low;
        int hi = n;
        if (lo >= n) {
            return files;
        }
        String mid = (by == FILE_PATH ? files.get((lo + hi) / 2).getAbsolutePath() :
                        (by == FILE_NAME ? files.get((lo + hi) / 2).getName() :
                            getExtension(files.get((lo + hi) / 2))));
        while (lo < hi) {
            while (lo < hi && (by == FILE_PATH ? files.get(lo).getAbsolutePath() :
                        (by == FILE_NAME ? files.get(lo).getName() :
                            getExtension(files.get(lo)))).compareTo(mid) < 0) {
                lo++;
            }
            while (lo < hi && (by == FILE_PATH ? files.get(hi).getAbsolutePath() :
                        (by == FILE_NAME ? files.get(hi).getName() :
                            getExtension(files.get(hi)))).compareTo(mid) < 0) {
                hi--;
            }
            if (lo < hi) {
                File T = files.get(lo);
                files.set(lo, files.get(hi));
                files.set(hi, T);
            }
        }
        if (hi < lo) {
            int T = hi;
            hi = lo;
            lo = T;
        }
        sort(files, low, lo, by);
        sort(files, lo == low ? lo + 1 : lo, n, by);
        return files;
    }

    /**
     * Compares the file to and array of extensions
     * {@code File} file - the file that extension is to be checked
     * {@code String[]} exts - the extensions to compare to
     *
     * @param file
     * @param exts
     * @return boolean
     */
    public static boolean isValidExtension(File file, String[] extensions) {
        return isValidExtension(file.getName(), extensions);
    }

    /**
     *
     * @param fileName
     * @param exts
     * @return
     */
    public static boolean isValidExtension(String fileName, String[] extensions) {
        return Pattern.compile("((.(?i)(" + ArrayUtils.toString(extensions, "|") + "))$)").matcher(getExtension(fileName)).matches();
    }
    
    /**
     * 
     * @param file
     * @param extension
     * @return 
     */
    public static boolean isValidExtension(File file, String extension) {
        return isValidExtension(file.getName(), extension);
    }
    
    /**
     * 
     * @param fileName
     * @param extension
     * @return 
     */
    public static boolean isValidExtension(String fileName, String extension) {
        return Pattern.compile("((.(?i)(" + extension + "))$)").matcher(getExtension(fileName)).matches();
    }

    /**
     * Gets the extension of the file
     *
     * @param file
     * @return String
     */
    public static String getExtension(File file) {
        return getExtension(file.getName());
    }

    /**
     * Gets the extension of the fileName
     *
     * @param fileName
     * @return String
     */
    public static String getExtension(String fileName) {
        Matcher matcher = Pattern.compile("((.(?i)([a-z]+))$)").matcher(fileName);
        return matcher.find() ? matcher.group() : "";
    }

    /**
     *
     * @param file
     * @return
     */
    public static String removeExtension(File file) {
        return removeExtension(file.getName());
    }

    /**
     *
     * @param fileName
     * @return
     */
    public static String removeExtension(String fileName) {
        return fileName.replace(getExtension(fileName), "");
    }

    /**
     *
     * @param list
     * @param dir
     * @param fileTypes
     * @return
     * @throws IOException
     */
    public static List<Image> getImages(List<Image> list, File dir, final String[] fileTypes) throws IOException {
        list = list != null ? list : new ArrayList<Image>();

        try {
            for (File file : dir.listFiles()) {
                if (file.isFile()) {
                    if (isValidExtension(file, fileTypes)) {
                        list.add(Toolkit.getDefaultToolkit().getImage(file.getAbsolutePath()));
                    }
                }
            }
        } catch (NullPointerException npe) {
            return list;
        }
        return list;
    }

    /**
     *
     * @param list
     * @param dir
     * @param fileTypes
     * @return
     * @throws IOException
     */
    public static List<BufferedImage> getBufferedImages(List<BufferedImage> list, File dir, final String[] fileTypes) throws IOException {
        list = list != null ? list : new ArrayList<BufferedImage>();
        
        for (File file : dir.listFiles()) {
            if (file.isFile()) {
                if (isValidExtension(file, fileTypes)) {
                    list.add(ImageIO.read(file));
                }
            }
        }
        return list;
    }
    
    /**
     * 
     * @param source
     * @param output
     * @return
     * @throws FileNotFoundException 
     */
    public static boolean copyFile(String source, String output) throws FileNotFoundException, IOException {
        return copyFile(new File(source), new File(output));
    }
    
    /**
     * 
     * @param source
     * @param output
     * @return
     * @throws FileNotFoundException 
     */
    public static boolean copyFile(File source, File output) {
        // Catch file copy exceptions
        try {
            // Open and auto-close source file
            try (FileInputStream fis = new FileInputStream(source)) {
                // Open and auto-close output file
                try (FileOutputStream fos = new FileOutputStream(output)) {
                    // Create buffer as byte array
                    byte[] buffer = new byte[1024];
                    // Copy the file content in bytes 
                    int length;
                    while ((length = fis.read(buffer)) > 0){
                        // Write bytes to buffer
                        fos.write(buffer, 0, length);
                    }
                }
            }
        } catch (FileNotFoundException fnfe) {
            System.out.println(fnfe.getMessage());
            return false;
        } catch (IOException ioe) {
            System.out.println(ioe.getMessage());
            return false;
        }
        return true;
    }
    
    /**
     * 
     * @param source
     * @param output
     * @return
     * @throws FileNotFoundException 
     */
    public static boolean moveFile(String source, String output) throws FileNotFoundException, IOException {
        return moveFile(new File(source), new File(output));
    }
    
    /**
     * 
     * @param source
     * @param output
     * @return
     * @throws FileNotFoundException 
     */
    public static boolean moveFile(File source, File output) throws FileNotFoundException, IOException {
        // Copy the source file
        if (copyFile(source, output)) {
            // Delete the source file
            if (!source.delete()) {
                output.delete();
                return true;
            }
        }
        return false;
    }
    
}
